/*
 * BTManager.java
 *
 * Created on 24 August 2007, 23:22
 *
 */

package digitrix.network.bt;

import digitrix.fightclub.game.FightClub;
import digitrix.network.bt.IBTCallback;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.bluetooth.DataElement;
import javax.bluetooth.DiscoveryAgent;
import javax.bluetooth.LocalDevice;
import javax.bluetooth.RemoteDevice;
import javax.bluetooth.ServiceRecord;
import javax.bluetooth.UUID;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.io.StreamConnectionNotifier;

/**
 *
 * @author Josh
 */
public class BTManager implements Runnable, IBTCallback {
    
    //<editor-fold defaultstate="collapsed" desc="BT Events">
    
    public static final byte EVT_REQUEST_FIGHT              = 0; 
    public static final byte EVT_ACCEPT                     = 1;
    public static final byte EVT_DECLINE                    = 2;
    public static final byte EVT_PLAYER_INFO                = 3; // [name,playerid,strength,speed,fights]
    public static final byte EVT_FIGHT                      = 4; 
    public static final byte EVT_GAME_DATA                  = 5; // ketstate
    public static final byte EVT_LOCAL_GAME_DATA            = 6; // ketstate
    public static final byte EVT_SYNC                       = 7;
    public static final byte EVT_SYNC_DATA                  = 8;
    public static final byte EVT_PAUSE                      = 9;
    public static final byte EVT_GAME_OVER                  = 10;
    public static final byte EVT_END                        = 11; 
    public static final byte EVT_RESTART                    = 12;
    public static final byte EVT_ACK                        = 13;
    public static final byte EVT_CONNECTION_LOST            = 14; 
    public static final byte EVT_NO_OPPONENTS_FOUND         = 15; 
    public static final byte EVT_READY                      = 16;
    
    public static final byte EVT_LAG_TEST_REQ               = 17; 
    public static final byte EVT_LAG_TEST_RES               = 19; 
    public static final byte EVT_LOCAL_LAG_TEST_FINISHED    = 20;
    
    public static final byte EVT_SYNC_TIME                  = 21; // every x seconds the server will sync it's time with the client
    public static final byte EVT_SYNC_DETAILS               = 22;    
    public static final byte EVT_SYNC_ROUND_OVER            = 23;
    
    public static final byte ROUND_OVER_LOCAL_WON           = 0; 
    public static final byte ROUND_OVER_REMOTE_WON          = 1; 
    
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="request challenge statuses">
    public static final byte REQ_CHAL_STATUS_PLAYER_NOT_FOUND           = 0;
    public static final byte REQ_CHAL_STATUS_UNABLE_TO_ESTABLISH_CXN    = 1;
    public static final byte REQ_CHAL_STATUS_SUCCESS                    = 2;
    public static final byte REQ_CHAL_STATUS_UNKNOWN_FAILURE            = 3;
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="states">
    
    public static final int STATE_SETUP                 = 0;
    public static final int STATE_GAME_READY            = 1; 
    public static final int STATE_LAG_TESTING           = 2;    
    
    //</editor-fold>
    
    public static final int LAG_TESTS = 10; 
    
    public static final int MAX_DEVICES = 4;
    public static final int MAX_DEVICE_DISCOVERY_RETRIES = 4;
    private static final String SERVICE_UUID = "00000000000010008000006057028c19"; 
    private static final boolean USE_SIMPLE_SERVICE_SEARCH = false; // set to false if you're wanting to scan all devices and then perform a service search - allows for more finite control over searching
    
    //<editor-fold defaultstate="collapsed" desc="singleton stuff">
    private static BTManager _instance = null; 
    
    public static BTManager getInstance(){
        if( _instance == null ){
            _instance = new BTManager(); 
        }
        
        return _instance; 
    }
    //</editor-fold>
    
    private IBTCallback _callback = null; 
    private BTListener _btListener = null; 
    private BTDispatcher _btDispatcher = null; 
    private boolean _isServer = false; 
    private boolean _isServerSet = false; 
    
    private int _pfdIndex = 0; // previouslyFoundDevices array index (where to index the next record) 
    private String[] _previouslyFoundDevices = null; // an array of BT addresses of the previously found devices 
    private LocalDevice _local = null;
    private DiscoveryAgent _agent = null;
    private String _url = null; 
    private String _playerName = null; 
    private DataOutputStream _dos = null; 
    private DataInputStream _dis = null; 
    private StreamConnection _cxn;    
    
    // server specific variables 
    private BTInquiryListener _inquiryListener = null; 
    private BTServiceListener _serviceListener = null; 
    private Hashtable _playerServiceMapping = null;
    
    private int _deviceDiscoveryRetry = 0; // counter for how many times we have tried to find our service with no luck
    
    private int _state = -1; 
    
    // variables used to determine the lag
    private int _lagTestCount = 0;
    private long _runningLag = 0;
    private long _requestTimeStamp = 0; 
    private int _lag = 0; // delay to process local event to keep it synced with the opponent
    
    /** Creates a new instance of BTManager */
    private BTManager() {
        //_isServer = isServer; 
    }  
    
    /**
     * to use this must be called; this will set up either the bt server or bt client respective to the isServer value  
     **/ 
    public void startService( String playerName ) throws Exception {
        if( !_isServerSet ){
            FightClub.getInstance().printError( "BTManager.startService()", "service will not start until _isServer is set" );
            throw new Exception( "service will not start until _isServer is set" );
        }
        else if( _callback == null ){
            FightClub.getInstance().printError( "BTManager.startService()", "service will not start until _callback is set" );
            throw new Exception( "service will not start until _callback is set" );
        }
        
        _lagTestCount = 0; 
        _playerName = playerName; 
        _state = STATE_SETUP; 
        
        if( _isServer ){
            _previouslyFoundDevices = new String[MAX_DEVICES*MAX_DEVICE_DISCOVERY_RETRIES]; 
            initServer( true );
        }
        else 
            initClient(); 
        
    }
    
    private void initClient(){
        System.out.println("initClient");
        
         try{
            // get the local bluetooth device
            _local = LocalDevice.getLocalDevice();
            
            // make this device discoverable
            _local.setDiscoverable(DiscoveryAgent.LIAC);
            
            // clients retrieve the discovery agent
            _agent = _local.getDiscoveryAgent();
            
            printDebugInfo( this, "initClient", "starting service with name " + _playerName, _isServer ); 
            
            // set the UUID name                        
            _url = "btspp://localhost:" + SERVICE_UUID + ";name=" + _playerName;
            
            new Thread( this ).start(); 
        }
        catch(Exception ex){
            printErrorInfo( this, "initClient", ex.toString(), _isServer ); 
        }    
    }
    
    private void initServer( boolean resetCounter ){
        if( resetCounter ){
            _pfdIndex = 0; 
            _deviceDiscoveryRetry = 0; 
        }
        
        new Thread( this ).start(); 
    }
    
    public void run(){
        if( _isServer ){
            if( USE_SIMPLE_SERVICE_SEARCH )
                startServerServiceSimple();
            else
                startServerService();
        }
        else 
            startClientService(); 
    }
    
    private void startClientService(){                
        printDebugInfo( this, "startClientService", "starting client service", _isServer );                       
            
        try{
            StreamConnectionNotifier notifier = (StreamConnectionNotifier)Connector.open(_url);
            
            // now wait for someone to connect
            /// blocks thread until something is recived
            _cxn = (StreamConnection)notifier.acceptAndOpen();
            
            // open the output stream
            _dos = _cxn.openDataOutputStream();
            
            // get input stream from server
            _dis = _cxn.openDataInputStream();                                                           
                        
            // create the listener and dispatcher 
            _btListener = new BTListener( this, _dis );
            _btDispatcher = new BTDispatcher( this, _dos );
            
        }
        catch(Exception ex){
            printErrorInfo( this, "startClientService", ex.toString(), _isServer );
        }
    }        
    
    /**
     * a cut-down version of startServerService for searching, will use the BT service search instead of querying all of the 
     * bt devices and querying their service records to find a match - this will only return one which will be the first one 
     * it finds - used when in a nosie area ie alot of bt devices buzzing around. 
     **/
    private void startServerServiceSimple(){
        printDebugInfo( this, "startServerServiceSimple", "starting server service", _isServer ); 
        
        try{            
            _agent = _local.getDiscoveryAgent(); 
            
            // now do a service search on all the devices found to see if any are currently playing our game 
            // set the UUID name            
            UUID uuid = new UUID( SERVICE_UUID, false ); 
                        
            String cxnURL = _agent.selectService( uuid, ServiceRecord.NOAUTHENTICATE_NOENCRYPT, true ); 
            
            if( cxnURL != null ){
                // try and open a connection 
                _cxn = (StreamConnection)Connector.open( cxnURL ); 
                                
                String[] players = new String[]{ "opponent" }; 
                
                // now lets pass the found player back to the server 
                btPlayersFound( players ); 
                
            }
            else{
                printDebugInfo( this, "startServiceSearchSimple", "connection is null", _isServer );                
                _callback.btEvent( EVT_NO_OPPONENTS_FOUND, -1 ); 
                return; 
            }                                                            
            
        }
        catch( Exception e ){
            printErrorInfo( this, "startServerService", e.toString(), _isServer ); 
            _callback.btEvent( EVT_NO_OPPONENTS_FOUND, -1 ); 
        }
        
    }
    
    private void startServerService(){
        printDebugInfo( this, "startServerService", "starting server service", _isServer ); 
        
        try{
            _local = LocalDevice.getLocalDevice();            
            _agent = _local.getDiscoveryAgent(); 
            
            // make this device discoverable
            _local.setDiscoverable( DiscoveryAgent.NOT_DISCOVERABLE );
            
            // create an inquiry listener 
            _inquiryListener = new BTInquiryListener( this ); 
            
            // find all of the bt devices around 
            // nb: we lock this thread on the inquiry object as we want this process to be 
            // synchronise (user flow and also makes it easier to develop) 
            synchronized( _inquiryListener ){
                printDebugInfo( this, "startServerService", "searching for bt devices", _isServer );
                
                // start limited access inquiry and install inquiry listener
                _agent.startInquiry( DiscoveryAgent.LIAC, _inquiryListener ); 
                
                // local this thread to wait for the inquiry listener to finish 
                _inquiryListener.wait();             
            }
            
            // if we have not found any devices then notify the user 
            if( !_inquiryListener.getFoundDevices().hasMoreElements() ){
                printDebugInfo( this, "startServiceSearch", "no devices found", _isServer );
                
                _callback.btEvent( EVT_NO_OPPONENTS_FOUND, -1 ); 
                return; 
            }            
            
            // now do a service search on all the devices found to see if any are currently playing our game 
            // set the UUID name            
            UUID[] uuid = new UUID[1];
            uuid[0] = new UUID( SERVICE_UUID, false ); 
            
            // retrieve service record- should include player name (service name)
            int attrbs[] = { 0x0100 };           
            Enumeration devices = _inquiryListener.getFoundDevices(); 
            _serviceListener = new BTServiceListener( this );
            
            // search each device
            while( devices.hasMoreElements() ){
                RemoteDevice rd = (RemoteDevice)devices.nextElement(); 
                
                
                // add device to our cached array to allow us to determine whether it has already been queried or not 
                _previouslyFoundDevices[_pfdIndex] = rd.getBluetoothAddress(); 
                _pfdIndex++; 
                
                // lock this thread to wait until each service search is complete 
                synchronized( _serviceListener ){
                    printDebugInfo( this, "startServerService", "performing a service search on the device " + rd.getBluetoothAddress(), _isServer );
                    
                    _agent.searchServices( attrbs, uuid, rd, _serviceListener );
                    // block this thread until we have finished search for our service 
                    _serviceListener.wait();
                }
            }
            
            // check to see if any of the devices found have the actual service running 
            if( _serviceListener.getFoundServiceRecordsVector().size() == 0 ){
                printDebugInfo( this, "startServiceSearch", "no devices found with our service running on it", _isServer );
                
                if( _deviceDiscoveryRetry < MAX_DEVICE_DISCOVERY_RETRIES ){
                    // try discoverying more devices by starting a new search thread (probably inefficent but hopefully not the norm) 
                    initServer( false ); 
                    _deviceDiscoveryRetry++;                     
                    return; // exit method 
                }
                else{
                    _callback.btEvent( EVT_NO_OPPONENTS_FOUND, -1 );
                    return; 
                }
            }
            
            // populate an array and pass it onto the callback method 
            _playerServiceMapping = new Hashtable( _serviceListener.getFoundServiceRecordsVector().size()+1 );             
            String[] players = new String[ _serviceListener.getFoundServiceRecordsVector().size()]; 
            int playersIndex = 0;            
            Enumeration enumServiceRec = _serviceListener.getFoundServiceRecords();
            while( enumServiceRec.hasMoreElements() ){
                
                ServiceRecord sr = (ServiceRecord)enumServiceRec.nextElement(); 
                // get the data element that should hold the players name 
                DataElement de = sr.getAttributeValue( 0x0100 ); 
                String playersName = (String)de.getValue();                 
                
                // search for duplicate player names (if so then amend players name by a integer 
                if( _playerServiceMapping.containsKey( playersName ) ){
                    int i = 1; 
                    while( !_playerServiceMapping.containsKey( playersName + i ) ){
                        i++; 
                    }
                    playersName += i; 
                }

                printDebugInfo( this, "startServerService", "adding player " + playersName, _isServer );

                // associate mapping 
                _playerServiceMapping.put( playersName, sr ); 

                // and finally add to the array 
                players[playersIndex] = playersName; 
                playersIndex++; 

                
            }
            
            // now lets pass the found player back to the server 
            btPlayersFound( players ); 
            
        }
        catch( Exception e ){
            printErrorInfo( this, "startServerService", e.toString(), _isServer ); 
        }
        
    }   
    
    public void startLagTest(){
        _state = STATE_LAG_TESTING; 
        
        dispatchNonParamEvent( EVT_LAG_TEST_REQ );
    }
    
    /**
     * this method is called from the server when given a list of players to select from, the player is able to select from a 
     * list and request to challenege him/her via the name. The service record that is used to establish the connection has been 
     * stored in a hashtable with the players name as the mapping/key variable/object 
     * @param playersName: the name of the player the user would like to request a challenge from 
     **/        
    public int requestChallenege( String playersName ){
        // does the player exist in the hashtable? 
        if( !_playerServiceMapping.containsKey( playersName ) )
            return REQ_CHAL_STATUS_PLAYER_NOT_FOUND;         
        
        // close any existing connections we might have open
        close(); 
        
        try{
            // the connection would already be set if the client is using a 'simple' search ie searching based on teh service uuid 
            // but will NOT be set if doing normal bt device and service discovery 
            if( _cxn == null ){
                ServiceRecord sr = (ServiceRecord)_playerServiceMapping.get( playersName );

                // retrieve url from players service record
                String url = sr.getConnectionURL( 0, false );

                // get the associate remote device 
                RemoteDevice rd = sr.getHostDevice(); 

                // open a stream connection 
                _cxn = (StreamConnection)Connector.open( url );
            }
            
            // open a input stream and create the bt listener 
            _dis = _cxn.openDataInputStream();
            _btListener = new BTListener( this, _dis ); 
            
            // open a output stream and create the bt dispatcher 
            _dos = _cxn.openDataOutputStream(); 
            _btDispatcher = new BTDispatcher( this, _dos ); 
            
            // request a fight 
            _btDispatcher.dispatchFightRequest( _playerName );                         
        }
        catch( Exception e ){
            printErrorInfo( this, "requestChallenge", e.toString(), _isServer );
            return REQ_CHAL_STATUS_UNKNOWN_FAILURE; 
        }
        
        return REQ_CHAL_STATUS_SUCCESS;
    }
    
    public boolean connectionAlive(){
        if( _btDispatcher == null || !_btDispatcher.isAlive() ||
                _btListener == null || !_btListener.isAlive() ){
            return false; 
        }
        else{
            return true; 
        }
    }
    
    public IBTCallback getBTManager(){
        return _callback; 
    }
    
    public void setCallback( IBTCallback btManager ){
        _callback = btManager; 
    }
    
    public void setIsServer( boolean isServer ){
        _isServerSet = true; 
        _isServer = isServer; 
    }
    
    public boolean isServer(){
        return _isServer && _isServerSet; 
    }
    
    public BTListener getBTListener(){
        return _btListener; 
    }

    public byte getEVT_ACCEPT() {
        return EVT_ACCEPT;
    }
    
    public BTDispatcher getBTDispatcher(){
        return _btDispatcher; 
    }
    
    public DiscoveryAgent getDiscoveryAgent(){
        return _agent; 
    }
    
    /**
     * @returns the lag between the message; this should be the delay 
     **/
    public int getLag(){
        return _lag; 
    }
    
    /**
     * @returns current state of the bluetooth manager
     **/
    public int getState(){
        return _state;
    }
    
    /**
     * helper method used to determine whether a device has already been queried from a previous sub-search 
     *(sub-search meaning a search that has been performed within the same request ie within the scope of the retries) 
     * @param btAddr: Bluetooth address of the device you would like to query
     **/
    public boolean deviceAlreadyExamined( String btAddr ){
        // first check to see if anything has been inserted yet
        if( _pfdIndex == 0 )
            return false; 
        
        for( int i=_pfdIndex-1; i>=0; i-- ){
            if( _previouslyFoundDevices[i].equalsIgnoreCase( btAddr ) )
                return true; 
        }
        
        return false; 
    }
    
    /**
     *
     * @param playerDetails: e.g. "name,playerid,strength,speed,fights"
     **/
    public void acceptChallenge( String playerDetails ){
        _btDispatcher.dispatchPlayerDetails( EVT_ACCEPT, playerDetails ); 
    }
    
    public void declineChallenge(){
        // decline inventation 
        _btDispatcher.dispatchNonParamEvent( EVT_DECLINE ); 
        
        // close our connection 
        close(); 
        
        // start again
        initClient(); 
    }
    
    /**
     * Call this method if you would like to finish the game or when the midlet is destoryed
     **/
    public void close(){
        if( _btDispatcher != null ){
            _btDispatcher.stop(); 
        }
        else if( _dos != null ){
            try{
                _dos.writeInt( EVT_END );
                _dos.flush(); 

                _dos.close();                  
            }
            catch( IOException ioe ){}
            _dos = null;
        }
        
        if( _btListener != null ){
            _btListener.stop(); 
        }
        else if( _dis != null ){
            try{
                _dis.close();                  
            }
            catch( IOException ioe ){}
            _dis = null;
        }
        
        if( _cxn != null ){
            try{
                _cxn.close();                
            }
            catch( IOException ioe ){}
            _cxn = null; 
        }
        
        _btDispatcher = null; 
        _btListener = null; 
    }        

    //<editor-fold defaultstate="collapsed" desc="methods for dispatching events - just delegate these to the dispatcher"> 
     public void dispatchGameData( int gameData ){
         if( _btDispatcher != null )
            _btDispatcher.dispatchGameData( gameData );
    }
     
     /**
      * (only called by the server)
      **/
     public void syncTime( int currentGameTime ){
         
     }

     /**
      * (only called by the server)
      **/
     public void syncRoundOver( int winner ){
         if( _btDispatcher != null )
             _btDispatcher.dispatchEvent( EVT_SYNC_ROUND_OVER, winner ); 
         
         printDebugInfo( this, "syncRoundOver", "sync round over", _isServer ); 
     }
     
     public void syncPlayerDetails( int posX, int posY, int health ){
         if( _btDispatcher != null )
             _btDispatcher.dispatchSyncPlayerDetails( posX, posY, health ); 
         
         printDebugInfo( this, "syncPlayerDetails", "sync local player details: x=" + posX + ", y=" + posY + ", h=" + health, _isServer ); 
     }
    
    public void dispatchNonParamEvent( int evt ){
        if( _btDispatcher != null ){
            if( evt == EVT_END )
                close(); 
            else if( evt == EVT_LAG_TEST_REQ ){                
                _lagTestCount++; 
                _requestTimeStamp = System.currentTimeMillis(); 
                _btDispatcher.dispatchNonParamEvent( evt );
            }
            else
                _btDispatcher.dispatchNonParamEvent( evt );
        }
    }
    
    /**
     *
     * @param playerDetails: e.g. "name,playerid,strength,speed,fights"
     **/
    public void dispatchPlayerDetails( int evt, String playerDetails ){
        if( _btDispatcher != null )
            _btDispatcher.dispatchPlayerDetails( evt, playerDetails );                 
    }
        
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="BTCallback method - just delegate these to the call object object"> 
    public void btSetStatus(String status) {
        if( _callback != null )
            _callback.btSetStatus( status ); 
    }

    public void btPlayerDetails(String details) {
        if( _callback != null )
            _callback.btPlayerDetails( details );
    }

    public void btEvent(byte evtType, int evtDetails) {
        if( _callback != null ){
            
            if( evtType == EVT_LAG_TEST_REQ ){
                // send back a response 
                _btDispatcher.dispatchNonParamEvent( EVT_LAG_TEST_RES ); 
            }
            else if( evtType == EVT_LAG_TEST_RES ){
                // record how long it took (add the elapsed time to the running total)
                _runningLag += System.currentTimeMillis() - _requestTimeStamp; 
                
                // finished ie have we done the pre-specified number of requests?
                if( _lagTestCount >= LAG_TESTS ){
                    // if so then calculate the lag and notify the caller
                    _lag = (int)_runningLag/LAG_TESTS; 
                    
                    _state = STATE_GAME_READY; 
                    
                    // print out the lag
                    printErrorInfo( this, "btEvent", "bluetooth lag is " + _lag + "ms", _isServer ); 
                    
                    _callback.btEvent( EVT_LOCAL_LAG_TEST_FINISHED, -1 );                    
                }
                else{
                    dispatchNonParamEvent( EVT_LAG_TEST_REQ );
                }
            } 
            else{            
                _callback.btEvent( evtType, evtDetails );
            }
        }
    }

    public void btPlayersFound(String[] players) {
        if( _callback != null )
            _callback.btPlayersFound( players ); 
    }
    
    public void btFightRequest(String playersName) {
        if( _callback != null )
            _callback.btFightRequest( playersName );
    }
    
    /**
     * sync remote player details; if we are the client then the game manager should response with their 
     * current details 
     **/
    public void btSyncEvent( int posX, int posY, int health ) {
        if( _callback != null )
            _callback.btSyncEvent( posX, posY, health );
        
        printDebugInfo( this, "btSyncEvent", "sync remote player details: x=" + posX + ", y=" + posY + ", h=" + health, _isServer ); 
    }
    
    //</editor-fold>
    
    //<editor-fold defaultstate="collapsed" desc="debug output method">
    private static final boolean DEBUG = true; 
    public static void printDebugInfo( Object o, String method, String msg, boolean isServer ){
        if( DEBUG ){
            if( isServer )
                System.out.println( "SERVER: " + o.getClass().getName() + "." + method + "() :: " + msg );
            else
                System.out.println( "CLIENT: " + o.getClass().getName() + "." + method + "() :: " + msg );
        }
    }
        
    public static void printErrorInfo( Object o, String method, String msg, boolean isServer ){
        if( DEBUG ){
            if( isServer )
                System.err.println( "SERVER: " + o.getClass().getName() + "." + method + "() :: " + msg );
            else
                System.err.println( "CLIENT: " + o.getClass().getName() + "." + method + "() :: " + msg );
        }
    }
    
    public static void printDebugInfo( Object o, String method, String msg ){
        if( DEBUG ){
            System.out.println( o.getClass().getName() + "." + method + "() :: " + msg );
        }
    }
        
    public static void printErrorInfo( Object o, String method, String msg ){
        if( DEBUG ){
            System.err.println( o.getClass().getName() + "." + method + "() :: " + msg );
        }
    }
    //</editor-fold>   
    
}
